package in.co.codedoc.sql;

import in.co.codedoc.cg.annotations.DBType;
import in.co.codedoc.cg.annotations.IsADBInputColumnMap;
import in.co.codedoc.cg.annotations.IsADBOutputColumnMap;
import in.co.codedoc.cg.annotations.IsAnIOCComponent;
import in.co.codedoc.ioc.Initializable;
import in.co.codedoc.sql.mapping.BigintLongOutputColumnMap;
import in.co.codedoc.sql.mapping.CharStringOutputColumnMap;
import in.co.codedoc.sql.mapping.DoubleDoubleInputColumnMap;
import in.co.codedoc.sql.mapping.DoubleDoubleOutputColumnMap;
import in.co.codedoc.sql.mapping.IntIntInputColumnMap;
import in.co.codedoc.sql.mapping.IntIntOutputColumnMap;
import in.co.codedoc.sql.mapping.LongBigintInputColumnMap;
import in.co.codedoc.sql.mapping.ShortSmallintInputColumnMap;
import in.co.codedoc.sql.mapping.SmallintShortOutputColumnMap;
import in.co.codedoc.sql.mapping.StringCharInputColumnMap;
import in.co.codedoc.util.PropertiesUtil;

import java.util.HashMap;
import java.util.Map;

@IsAnIOCComponent
@SuppressWarnings("unchecked")
public class DBMapUtil
	implements Initializable
{
	public DBInputColumnMap GetInputColumnMap(short v)
	{
		return GetInputColumnMap(v,DBType.SMALLINT);
	}
	
	public DBInputColumnMap GetInputColumnMap(short v,DBType dbType)
	{
		Class<? extends DBInputColumnMap> t = GetInputColumnMap(Short.TYPE, dbType);
		try
		{
			return t.getConstructor(new Class[]{Short.TYPE}).newInstance(new Object[]{v});
		}
		catch(Throwable th)
		{
			throw new RuntimeException
			(
				"Failed to instantiate the input column map of type " + t + ":" + th.getMessage(),
				th
			);
		}
	}

	public DBInputColumnMap GetInputColumnMap(int v)
	{
		return GetInputColumnMap(v,DBType.INT);
	}
	
	public DBInputColumnMap GetInputColumnMap(int v,DBType dbType)
	{
		Class<? extends DBInputColumnMap> t = GetInputColumnMap(Integer.TYPE, dbType);
		try
		{
			return t.getConstructor(new Class[]{Integer.TYPE}).newInstance(new Object[]{v});
		}
		catch(Throwable th)
		{
			throw new RuntimeException
			(
				"Failed to instantiate the input column map of type " + t + ":" + th.getMessage(),
				th
			);
		}
	}
	
	public DBInputColumnMap GetInputColumnMap(long v)
	{
		return GetInputColumnMap(v,DBType.BIGINT);
	}

	public DBInputColumnMap GetInputColumnMap(IDBase v)
	{
		return GetInputColumnMap(v,DBType.BIGINT);
	}
	
	public DBInputColumnMap GetInputColumnMap(IDBase v,DBType dbType)
	{
		return GetInputColumnMap(v == null ? Long.MIN_VALUE : v.GetId(),dbType);
	}
	
	public DBInputColumnMap GetInputColumnMap(long v,DBType dbType)
	{
		Class<? extends DBInputColumnMap> t = GetInputColumnMap(Long.TYPE, dbType);
		try
		{
			return t.getConstructor(new Class[]{Long.TYPE}).newInstance(new Object[]{v});
		}
		catch(Throwable th)
		{
			throw new RuntimeException
			(
				"Failed to instantiate the input column map of type " + t + ":" + th.getMessage(),
				th
			);
		}
	}
	
	public DBInputColumnMap GetInputColumnMap(double v)
	{
		return GetInputColumnMap(v,DBType.DOUBLE);
	}
	
	public DBInputColumnMap GetInputColumnMap(double v,DBType dbType)
	{
		Class<? extends DBInputColumnMap> t = GetInputColumnMap(Double.TYPE, dbType);
		try
		{
			return t.getConstructor(new Class[]{Double.TYPE}).newInstance(new Object[]{v});
		}
		catch(Throwable th)
		{
			throw new RuntimeException
			(
				"Failed to instantiate the input column map of type " + t + ":" + th.getMessage(),
				th
			);
		}
	}
	
	public DBInputColumnMap GetInputColumnMap(String v)
	{
		return GetInputColumnMap(v, DBType.CHAR);
	}
	
	public DBInputColumnMap GetInputColumnMap(Object v,DBType dbType)
	{
		Class<? extends DBInputColumnMap> mapType = null;
		Class valueType = null;
		
		if(v == null)
		{
			ClassAndDBType classAndDBType = FindFirstInputMap(dbType);
			valueType = classAndDBType.javaType;
		}
		else
		{
			valueType = v.getClass();
		}
		
		mapType = GetInputColumnMap(valueType, dbType);

		if(mapType == null)
		{
			throw new RuntimeException("Failed to find a map for ");
		}
		
		try
		{
			return mapType.getConstructor(new Class[]{valueType}).newInstance(new Object[]{v});
		}
		catch(Throwable th)
		{
			throw new RuntimeException
			(
				"Failed to instantiate the input column map of type " + mapType + ":" + th.getMessage(),
				th
			);
		}
	}
	
	private ClassAndDBType FindFirstInputMap(DBType dbType)
	{
		for(ClassAndDBType key:inputMapByTypes.keySet())
		{
			if(key.dbType == dbType)
			{
				return key;
			}
		}
		return null;
	}

	public Class<? extends DBInputColumnMap> GetInputColumnMap(Class javaType,DBType dbType)
	{
		ClassAndDBType cdt = new ClassAndDBType(javaType,dbType);
		
		if(inputMapByTypes.containsKey(cdt))
		{
			return inputMapByTypes.get(cdt);
		}
		else
		{
			if(Logger.logger.isDebugEnabled())
			{
				Logger.logger.debug("No DBInputColumnMap which maps a '" + javaType.getCanonicalName() + "' to a '" + dbType + "'");
				Logger.logger.debug("Input Maps At this point:" + inputMapByTypes);
			}
			
			throw new RuntimeException
			(
				"No DBInputColumnMap which maps a '" + javaType.getCanonicalName() + "' to a '" + dbType + "'"
			);
		}
	}

	public DBOutputColumnMap GetOutputColumnMap(DBType dbType,Class javaType)
	{
		ClassAndDBType cdt = new ClassAndDBType(javaType,dbType);
		
		if(outputMapByTypes.containsKey(cdt))
		{
			try
			{
				return outputMapByTypes.get(cdt).newInstance();
			}
			catch(Throwable th)
			{
				throw new RuntimeException
				(
					"Failed to instantiate " + outputMapByTypes.get(cdt) + ":" + th.getMessage(),
					th
				);
			}
		}
		else
		{
			if(Logger.logger.isDebugEnabled())
			{
				Logger.logger.debug("No DBOutputColumnMap which maps a '" + dbType + "' to a '" + javaType.getCanonicalName() + "'");
				Logger.logger.debug("Output Maps At this point:" + outputMapByTypes);
			}
			throw new RuntimeException
			(
				"No DBOutputColumnMap which maps a '" + dbType + "' to a '" + javaType.getCanonicalName() + "'"
			);
		}
	}
	
	@Override
	public void Initialize()
	{
		inputMapByTypes.put(new ClassAndDBType(String.class,DBType.CHAR),StringCharInputColumnMap.class);
		inputMapByTypes.put(new ClassAndDBType(Short.class,DBType.SMALLINT),ShortSmallintInputColumnMap.class);
		inputMapByTypes.put(new ClassAndDBType(Short.TYPE,DBType.SMALLINT),ShortSmallintInputColumnMap.class);
		inputMapByTypes.put(new ClassAndDBType(Integer.class,DBType.INT),IntIntInputColumnMap.class);
		inputMapByTypes.put(new ClassAndDBType(Integer.TYPE,DBType.INT),IntIntInputColumnMap.class);
		inputMapByTypes.put(new ClassAndDBType(Long.class,DBType.BIGINT),LongBigintInputColumnMap.class);
		inputMapByTypes.put(new ClassAndDBType(Long.TYPE,DBType.BIGINT),LongBigintInputColumnMap.class);
		inputMapByTypes.put(new ClassAndDBType(Double.class,DBType.DOUBLE),DoubleDoubleInputColumnMap.class);
		inputMapByTypes.put(new ClassAndDBType(Double.TYPE,DBType.DOUBLE),DoubleDoubleInputColumnMap.class);

		outputMapByTypes.put(new ClassAndDBType(String.class,DBType.CHAR),CharStringOutputColumnMap.class);
		outputMapByTypes.put(new ClassAndDBType(Short.class,DBType.SMALLINT),SmallintShortOutputColumnMap.class);
		outputMapByTypes.put(new ClassAndDBType(Short.TYPE,DBType.SMALLINT),SmallintShortOutputColumnMap.class);
		outputMapByTypes.put(new ClassAndDBType(Integer.class,DBType.INT),IntIntOutputColumnMap.class);
		outputMapByTypes.put(new ClassAndDBType(Integer.TYPE,DBType.INT),IntIntOutputColumnMap.class);
		outputMapByTypes.put(new ClassAndDBType(Long.class,DBType.BIGINT),BigintLongOutputColumnMap.class);
		outputMapByTypes.put(new ClassAndDBType(Long.TYPE,DBType.BIGINT),BigintLongOutputColumnMap.class);
		outputMapByTypes.put(new ClassAndDBType(Double.class,DBType.DOUBLE),DoubleDoubleOutputColumnMap.class);
		outputMapByTypes.put(new ClassAndDBType(Double.TYPE,DBType.DOUBLE),DoubleDoubleOutputColumnMap.class);

		PropertiesUtil.ScanClassPath
		(
			"db-maps.properties",
			new PropertiesUtil.ForEach()
			{
				@Override
				public void Do(String key, String value)
				{
					try
					{
						Class t = Class.forName(key);
						if(DBInputColumnMap.class.isAssignableFrom(t))
						{
							IsADBInputColumnMap anno
								= (IsADBInputColumnMap) t.getAnnotation(IsADBInputColumnMap.class);
			
							if(Logger.logger.isDebugEnabled())
							{
								Logger.logger.debug
								(
									"DBInputColumnMap " + t.getCanonicalName() +
									" is the designated map for " + anno.javaType().getCanonicalName() +
									"-->" + anno.dbType()
								);
							}
							
							inputMapByTypes.put
							(
								new ClassAndDBType(anno.javaType(),anno.dbType()),
								t
							);
						}
						else if(DBOutputColumnMap.class.isAssignableFrom(t))
						{
							IsADBOutputColumnMap anno
								= (IsADBOutputColumnMap) t.getAnnotation(IsADBOutputColumnMap.class);
							
							if(Logger.logger.isDebugEnabled())
							{
								Logger.logger.debug
								(
									"DBOutputColumnMap " + t.getCanonicalName() +
									" is the designated map for " + anno.dbType() +
									"-->" + anno.javaType().getCanonicalName()
								);
							}
							
							outputMapByTypes.put
							(
								new ClassAndDBType(anno.javaType(),anno.dbType()),
								t
							);
						}
						else
						{
							Logger.logger.error
							(
								"db-maps must either extend DBInputColumnMap or DBOutputColumnMap. " +
								t + " seems to be neither. Ignored."
							);
						}
					}
					catch (Throwable th)
					{
						Logger.logger.error("Failed to register db maps in class " + key,th);
					}
				}
			}
		);
	}

	private static class ClassAndDBType
	{
		public ClassAndDBType(Class javaType,DBType dbType)
		{
			this.javaType = javaType;
			this.dbType = dbType;
		}
		
		public int hashCode()
		{
			return javaType.hashCode()^dbType.hashCode();
		}
		
		public boolean equals(Object t)
		{
			if(t == null || !(t instanceof ClassAndDBType))
			{
				return false;
			}
			
			ClassAndDBType that = (ClassAndDBType) t;
			return javaType.equals(that.javaType) && dbType.equals(that.dbType);
		}
		
		public String toString()
		{
			return "<" + javaType.getCanonicalName() + "," + dbType + ">";
		}
		
		private Class javaType;
		private DBType dbType;
	}

	private Map<ClassAndDBType,Class<? extends DBInputColumnMap>> inputMapByTypes
		= new HashMap<ClassAndDBType, Class<? extends DBInputColumnMap>>();
	private Map<ClassAndDBType,Class<? extends DBOutputColumnMap>> outputMapByTypes
		= new HashMap<ClassAndDBType, Class<? extends DBOutputColumnMap>>();
}
